package com.kingdee.fpy;/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.pdfbox.cos.*;
import org.apache.pdfbox.io.RandomAccessRead;
import org.apache.pdfbox.io.ScratchFile;
import org.apache.pdfbox.pdfparser.PDFObjectStreamParser;
import org.apache.pdfbox.pdfparser.PDFParser;

import java.io.IOException;
import java.io.InputStream;
import java.util.*;

import static org.apache.pdfbox.util.Charsets.ISO_8859_1;

public class PDFParserFpy extends PDFParser {
    private static final Log LOG = LogFactory.getLog(PDFParserFpy.class);

    /**
     * is parser using auto healing capacity ?
     */
    private boolean isLenient = true;

    // 排除列表，用于减少分析次数
    final List<COSName> excludeList = Arrays.asList(
        COSName.FONT, COSName.CONTENTS, COSName.MEDIA_BOX, COSName.ANNOTS, COSName.METADATA);

    public PDFParserFpy(RandomAccessRead source, String decryptionPassword, InputStream keyStore,
                        String alias, ScratchFile scratchFile) throws IOException {
        super(source, decryptionPassword, keyStore, alias);
    }

    protected void initialParse() throws IOException {
        COSDictionary trailer = retrieveTrailer();

        // 去除不必要对象，只保留root
        if (!trailer.containsKey("Filter"))
            retainDictItem(trailer, COSName.ROOT);
        else
            removeUnusedKeys(trailer, COSName.INFO, COSName.PREV, COSName.ID);

        COSBase base = parseTrailerValuesDynamically(trailer);
        if (!(base instanceof COSDictionary)) {
            throw new IOException("Expected root dictionary, but got this: " + base);
        }

        COSDictionary root = (COSDictionary) base;
        // 去除不必要对象，
        retainDictItem(root, COSName.PAGES, COSName.TYPE);

        // in some pdfs the type value "Catalog" is missing in the root object
        if (isLenient() && !root.containsKey(COSName.TYPE)) {
            root.setItem(COSName.TYPE, COSName.CATALOG);
        }
        // parse all objects, starting at the root dictionary
        parseDictObjects(root, (COSName[]) null);

        document.setDecrypted();
        initialParseDone = true;
    }

    void removeUnusedKeys(COSDictionary obj) {
        for (COSName n : excludeList) {
            obj.removeItem(n);
        }
    }

    void removeUnusedKeys(COSDictionary obj, COSName... keys) {
        for (COSName n : keys) {
            obj.removeItem(n);
        }
    }

    // 清空obj的内容，只保留retain外
    void retainDictItem(COSDictionary obj, COSName... retains) {
        Set<COSName> set = new HashSet<COSName>(Arrays.asList(retains));
        for (COSName k : obj.keySet()) {
            if (set.contains(k))
                continue;
            obj.removeItem(k);
        }
    }

    // 去掉了所有关于bfSearchCOSObjectKeyOffsets的逻辑，意义不大
    protected void parseDictObjects(COSDictionary dict, COSName... excludeObjects) throws IOException {

        // ---- create queue for objects waiting for further parsing
        final Queue<COSBase> toBeParsedList = new LinkedList<COSBase>();
        // offset ordered object map
        final TreeMap<Long, List<COSObject>> objToBeParsed = new TreeMap<Long, List<COSObject>>();
        // in case of compressed objects offset points to stmObj
        final Set<Long> parsedObjects = new HashSet<Long>();
        final Set<Long> addedObjects = new HashSet<Long>();

        addExcludedToList(excludeObjects, dict, parsedObjects);
        addNewToList(toBeParsedList, dict.getValues(), addedObjects);

        // ---- go through objects to be parsed
        while (!(toBeParsedList.isEmpty() && objToBeParsed.isEmpty())) {
            // -- first get all COSObject from other kind of objects and
            // put them in objToBeParsed; afterwards toBeParsedList is empty
            COSBase baseObj;
            while ((baseObj = toBeParsedList.poll()) != null) {
                if (baseObj instanceof COSDictionary) {
                    // 去除不需要的key，降低解析次数
                    removeUnusedKeys((COSDictionary) baseObj);

                    addNewToList(toBeParsedList, ((COSDictionary) baseObj).getValues(), addedObjects);
                } else if (baseObj instanceof COSArray) {
                    for (COSBase cosBase : (COSArray) baseObj) {
                        addNewToList(toBeParsedList, cosBase, addedObjects);
                    }
                } else if (baseObj instanceof COSObject) {
                    COSObject obj = (COSObject) baseObj;
                    long objId = getObjectId(obj);
                    COSObjectKey objKey = new COSObjectKey(obj.getObjectNumber(), obj.getGenerationNumber());

                    if (!parsedObjects.contains(objId)) {
                        Long fileOffset = document.getXrefTable().get(objKey);

                        // it is allowed that object references point to null,
                        // thus we have to test
                        if (fileOffset != null && fileOffset != 0) {
                            if (fileOffset > 0) {
                                objToBeParsed.put(fileOffset, Collections.singletonList(obj));
                            } else {
                                // negative offset means we have a compressed
                                // object within object stream => get offset of object stream
                                COSObjectKey key = new COSObjectKey((int) -fileOffset, 0);
                                fileOffset = document.getXrefTable().get(key);

                                List<COSObject> stmObjects = objToBeParsed.get(fileOffset);
                                if (stmObjects == null) {
                                    stmObjects = new ArrayList<COSObject>();
                                    objToBeParsed.put(fileOffset, stmObjects);
                                }
                                // java does not have a test for immutable
                                else if (!(stmObjects instanceof ArrayList)) {
                                    throw new IOException(obj + " cannot be assigned to offset " +
                                        fileOffset + ", this belongs to " + stmObjects.get(0));
                                }
                                stmObjects.add(obj);
                            }
                        } else {
                            // NULL object
                            COSObject pdfObject = document.getObjectFromPool(objKey);
                            pdfObject.setObject(COSNull.NULL);
                        }
                    }
                }
            }

            // ---- read first COSObject with smallest offset
            // resulting object will be added to toBeParsedList
            if (objToBeParsed.isEmpty()) {
                break;
            }

            for (COSObject obj : objToBeParsed.remove(objToBeParsed.firstKey())) {
                COSBase parsedObj = parseObjectDynamically(obj.getObjectNumber(),
                    obj.getGenerationNumber(), false);

                if (parsedObj != null) {
                    obj.setObject(parsedObj);
                    addNewToList(toBeParsedList, parsedObj, addedObjects);
                    parsedObjects.add(getObjectId(obj));
                }

                if (checkImgAndNotQR(parsedObj))
                    return;
            }
        }
    }

    protected COSBase parseObjectDynamically(long objNr, int objGenNr,
                                             boolean requireExistingNotCompressedObj) throws IOException {
        // ---- create object key and get object (container) from pool
        final COSObjectKey objKey = new COSObjectKey(objNr, objGenNr);
        final COSObject pdfObject = document.getObjectFromPool(objKey);

        if (pdfObject.getObject() == null) {
            // not previously parsed
            // ---- read offset or object stream object number from xref table
            Long offsetOrObjstmObNr = document.getXrefTable().get(objKey);

            if (offsetOrObjstmObNr == null) {
                // not defined object -> NULL object (Spec. 1.7, chap. 3.2.9)
                pdfObject.setObject(COSNull.NULL);
            } else if (offsetOrObjstmObNr > 0) {
                // offset of indirect object in file
                parseFileObject(offsetOrObjstmObNr, objKey, pdfObject);
            } else {
                // xref value is object nr of object stream containing object to be parsed
                // since our object was not found it means object stream was not parsed so far
                parseObjectStream((int) -offsetOrObjstmObNr);
            }
        }
        return pdfObject.getObject();
    }

    protected void parseFileObject(Long offsetOrObjstmObNr, final COSObjectKey objKey, final COSObject pdfObject) throws IOException {
        // ---- go to object start
        source.seek(offsetOrObjstmObNr);

        // ---- we must have an indirect object
        final long readObjNr = readObjectNumber();
        final int readObjGen = readGenerationNumber();
        readExpectedString(OBJ_MARKER, true);

        // ---- consistency check
        if ((readObjNr != objKey.getNumber()) || (readObjGen != objKey.getGeneration())) {
            throw new IOException("XREF for " + objKey.getNumber() + ":"
                + objKey.getGeneration() + " points to wrong object: " + readObjNr
                + ":" + readObjGen + " at offset " + offsetOrObjstmObNr);
        }

        skipSpaces();
        COSBase pb = parseDirObject();
        String endObjectKey = readString();

        if (endObjectKey.equals(STREAM_STRING)) {
            // 如果图片，且不是二维码，则直接返回
            if (checkImgAndNotQR(pb))
                return;

            source.rewind(endObjectKey.getBytes(ISO_8859_1).length);
            if (pb instanceof COSDictionary) {
                COSStream stream = parseCOSStream((COSDictionary) pb);

                if (securityHandler != null) {
                    securityHandler.decryptStream(stream, objKey.getNumber(), objKey.getGeneration());
                }
                pb = stream;
            } else {
                // this is not legal
                // the combination of a dict and the stream/endstream
                // forms a complete stream object
                throw new IOException("Stream not preceded by dictionary (offset: "
                    + offsetOrObjstmObNr + ").");
            }
            skipSpaces();
            endObjectKey = readLine();

            // we have case with a second 'endstream' before endobj
            if (!endObjectKey.startsWith(ENDOBJ_STRING) && endObjectKey.startsWith(ENDSTREAM_STRING)) {
                endObjectKey = endObjectKey.substring(9).trim();
                if (endObjectKey.length() == 0) {
                    // no other characters in extra endstream line
                    // read next line
                    endObjectKey = readLine();
                }
            }
        } else if (securityHandler != null) {
            securityHandler.decrypt(pb, objKey.getNumber(), objKey.getGeneration());
        }

        pdfObject.setObject(pb);

        if (!endObjectKey.startsWith(ENDOBJ_STRING)) {
            LOG.warn("Object (" + readObjNr + ":" + readObjGen + ") at offset "
                + offsetOrObjstmObNr + " does not end with 'endobj' but with '"
                + endObjectKey + "'");
        }
    }

    private void parseObjectStream(int objstmObjNr) throws IOException {
        final COSBase objstmBaseObj = parseObjectDynamically(objstmObjNr, 0, true);
        if (objstmBaseObj instanceof COSStream) {
            // parse object stream
            PDFObjectStreamParser parser;
            try {
                parser = new PDFObjectStreamParser((COSStream) objstmBaseObj, document);
            } catch (IOException ex) {
                if (isLenient) {
                    LOG.error("object stream " + objstmObjNr + " could not be parsed due to an exception", ex);
                    return;
                } else {
                    throw ex;
                }
            }

            try {
                parser.parse();
            } catch (IOException exception) {
                if (isLenient) {
                    LOG.debug("Stop reading object stream " + objstmObjNr + " due to an exception", exception);
                    // the error is handled in parseDictObjects
                    return;
                } else {
                    throw exception;
                }
            }
            // register all objects which are referenced to be contained in object stream
            for (COSObject next : parser.getObjects()) {
                COSObjectKey stmObjKey = new COSObjectKey(next);
                Long offset = xrefTrailerResolver.getXrefTable().get(stmObjKey);
                if (offset != null && offset == -objstmObjNr) {
                    COSObject stmObj = document.getObjectFromPool(stmObjKey);
                    stmObj.setObject(next.getObject());
                }
            }
        }
    }

    // 如果是非二维码图片，则返回true，其他返回false
    protected Boolean checkImgAndNotQR(COSBase parsedObj) {
        if (parsedObj instanceof COSDictionary) {
            COSDictionary d = (COSDictionary) parsedObj;
            COSName subtype = d.getCOSName(COSName.SUBTYPE);
            COSName type = d.getCOSName(COSName.TYPE);
            COSArray bbox = d.getCOSArray(COSName.BBOX);

            // 不是图片则返回false
            if(subtype != null && !subtype.equals(COSName.IMAGE))
                return false;

            if(type != null && !type.equals(COSName.XOBJECT))
                return false;

            // 是图片且长宽是相当
            if (d.getInt("Width", 0) == d.getInt("Height"))
                return false;

            if (bbox != null && bbox.getInt(2) == bbox.getInt(3))
                return false;

            return true;
        }
        return false;
    }

    // add objects not to be parsed to list of already parsed objects
    private void addExcludedToList(COSName[] excludeObjects, COSDictionary dict, final Set<Long> parsedObjects) {
        if (excludeObjects != null) {
            for (COSName objName : excludeObjects) {
                COSBase baseObj = dict.getItem(objName);
                if (baseObj instanceof COSObject) {
                    parsedObjects.add(getObjectId((COSObject) baseObj));
                }
            }
        }
    }

    private void addNewToList(final Queue<COSBase> toBeParsedList, final COSBase newObject,
                              final Set<Long> addedObjects) {
        if (newObject instanceof COSObject) {
            final long objId = getObjectId((COSObject) newObject);
            if (!addedObjects.add(objId)) {
                return;
            }
            toBeParsedList.add(newObject);
        } else if (newObject instanceof COSDictionary || newObject instanceof COSArray) {
            toBeParsedList.add(newObject);
        }
    }

    /**
     * Creates a unique object id using object number and object generation
     * number. (requires object number &lt; 2^31))
     */
    private long getObjectId(final COSObject obj) {
        return obj.getObjectNumber() << 32 | obj.getGenerationNumber();
    }

    /**
     * Adds all from newObjects to toBeParsedList if it is not an COSObject or
     * we didn't add this COSObject already (checked via addedObjects).
     */
    private void addNewToList(final Queue<COSBase> toBeParsedList,
                              final Collection<COSBase> newObjects, final Set<Long> addedObjects) {
        for (COSBase newObject : newObjects) {
            addNewToList(toBeParsedList, newObject, addedObjects);
        }
    }
}
